package ascii;

import java.awt.image.BufferedImage;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.io.*;
import java.nio.charset.Charset;

/**
 * Convert image to ASCII art for BBS
 * @author Guan-Zhong Huang
 * @date 2010/01/01
 * @version 0.1
 * @since 0.1
 */
public class AsciiConverter {

    /**
     * Intermediate scaled image
     */
    private BufferedImage mediateImg;
    /**
     * Image quantized to only 16 colors
     */
    private byte[][] quanImg;

    private float[] rgb2xyY(int R, int G, int B, float[] xyY) {
        /*
         * Reference
         * http://www.brucelindbloom.com
         * http://www.f4.fhtw-berlin.de/~barthel/ImageJ/ColorInspector//HTMLHelp/farbraumJava.htm
         * http://en.wikipedia.org/wiki/SRGB
         */
        float rf, gf, bf;
        float r, g, b, X, Y, Z;

        // RGB to XYZ
        r = R / 255.f; //R 0..1
        g = G / 255.f; //G 0..1
        b = B / 255.f; //B 0..1

        if (r <= 0.04045) {
            r = r / 12;
        } else {
            r = (float) Math.pow((r + 0.055) / 1.055, 2.4);
        }
        if (g <= 0.04045) {
            g = g / 12;
        } else {
            g = (float) Math.pow((g + 0.055) / 1.055, 2.4);
        }
        if (b <= 0.04045) {
            b = b / 12;
        } else {
            b = (float) Math.pow((b + 0.055) / 1.055, 2.4);
        }

        X = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b;
        Y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b;
        Z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b;

        float x;
        float y;
        float sum = X + Y + Z;

        if (sum != 0) {
            x = X / sum;
            y = Y / sum;
        } else {
            float Xr = 0.964221f;  // reference white
            float Yr = 1.0f;
            float Zr = 0.825211f;

            x = Xr / (Xr + Yr + Zr);
            y = Yr / (Xr + Yr + Zr);
        }

        if (xyY == null) {
            xyY = new float[3];
        }
        xyY[0] = x;
        xyY[1] = y;
        xyY[2] = Y;
        return xyY;
    }

    private float colorDistance(int rgbval1, int rgbval2) {
        float[] xyYval1 = rgb2xyY(
                (rgbval1 >> 16) & 0xff, (rgbval1 >> 8) & 0xff,
                rgbval1 & 0xff, null);
        float[] xyYval2 = rgb2xyY(
                (rgbval2 >> 16) & 0xff, (rgbval2 >> 8) & 0xff,
                rgbval2 & 0xff, null);

        float xdiff = xyYval1[0] - xyYval2[0];
        float ydiff = xyYval1[1] - xyYval2[1];
        float Ydiff = xyYval1[2] - xyYval2[2];
        return 4 * xdiff * xdiff + 4 * ydiff * ydiff + Ydiff * Ydiff;
    }

    private int quantizeImagePixel(int x, int y) {
        int rgbval = mediateImg.getRGB(x, y);
        int min_i = 0;
        float min = colorDistance(rgbval, AsciiArt.colors[0].getRGB());
        for (int i = 1; i < AsciiArt.colors.length; ++i) {
            float d = colorDistance(rgbval, AsciiArt.colors[i].getRGB());
            if (d < min) {
                min_i = i;
                min = d;
            }
        }
        rgbval = AsciiArt.colors[min_i].getRGB();
        mediateImg.setRGB(x, y, rgbval);
        return rgbval;
    }

    private void quantizeImage() {
        quanImg = new byte[mediateImg.getHeight()][mediateImg.getWidth()];
        for (int x = 0; x < mediateImg.getWidth(); ++x) {
            for (int y = 0; y < mediateImg.getHeight(); ++y) {
                int rgbval = quantizeImagePixel(x, y);
                int i;
                for (i = 0; i < AsciiArt.colors.length; ++i) {
                    if (rgbval == AsciiArt.colors[i].getRGB()) {
                        break;
                    }
                }
                if (i == AsciiArt.colors.length) {
                    System.err.println("Color quantization failure");
                } else {
                    quanImg[y][x] = (byte) i;
                }
            }
        }
    }

    private String getMatchedBlock(int gridX, int gridY, int color1_i,
            int color2_i, int color1_count) {
        int ch1_i = Math.round(color1_count / 64.0f * 8.0f) % 8;
        int ch2_i = Math.round((1.0f - color1_count / 64.0f) * 8.0f) % 8;
        int sim1_i = 0;
        float sim1 = AsciiArt.chars[ch1_i][0].matchGrid(
                quanImg, gridX, gridY, color1_i, color2_i);
        for (int i = 1; i < AsciiArt.chars[ch1_i].length; ++i) {
            float sim = AsciiArt.chars[ch1_i][i].matchGrid(
                    quanImg, gridX, gridY, color1_i, color2_i);
            if (sim > sim1) {
                sim1 = sim;
                sim1_i = i;
            }
        }
        int sim2_i = 0;
        float sim2 = AsciiArt.chars[ch2_i][0].matchGrid(
                quanImg, gridX, gridY, color2_i, color1_i);
        for (int i = 1; i < AsciiArt.chars[ch2_i].length; ++i) {
            float sim = AsciiArt.chars[ch2_i][i].matchGrid(
                    quanImg, gridX, gridY, color2_i, color1_i);
            if (sim > sim2) {
                sim2 = sim;
                sim2_i = i;
            }
        }
        if (sim1 > sim2) {
            return AsciiArt.chars[ch1_i][sim1_i].getAnsiCode(color1_i, color2_i);
        } else {
            return AsciiArt.chars[ch2_i][sim2_i].getAnsiCode(color2_i, color1_i);
        }
    }

    private String transformGrid(int startX, int startY) {
        int[] count = new int[AsciiArt.colors.length];
        for (int i = 0; i < count.length; ++i) {
            count[i] = 0;
        }
        for (int i = 0; i < 8; ++i) {
            for (int j = 0; j < 8; ++j) {
                ++count[quanImg[startY + i][startX + j]];
            }
        }
        int max_i, max, max_i2, max2;
        if (count[0] > count[1]) {
            max_i = 0;
            max = count[0];
            max_i2 = 1;
            max2 = count[1];
        } else {
            max_i = 1;
            max = count[1];
            max_i2 = 0;
            max2 = count[0];
        }
        for (int i = 2; i < count.length; ++i) {
            if (count[i] > max) {
                max_i2 = max_i;
                max2 = max;
                max_i = i;
                max = count[i];
            } else if (count[i] > max2) {
                max_i2 = i;
                max2 = count[i];
            }
        }
        // quantize grid to contain only 2 colors
        for (int i = 0; i < 8; ++i) {
            for (int j = 0; j < 8; ++j) {
                if (quanImg[startY + i][startX + j] != max_i) {
                    quanImg[startY + i][startX + j] = (byte) max_i2;
                }
            }
        }
        return getMatchedBlock(startX, startY, max_i, max_i2, max);
    }

    private void transformImage(OutputStreamWriter osw) {
        StringBuilder builder = new StringBuilder();
        for (int y = 0; y < quanImg.length; y += 8) {
            for (int x = 0; x < quanImg[y].length; x += 8) {
                builder.append(transformGrid(x, y));
            }
            builder.append('\n');
            try {
                osw.write(builder.toString());
                /* If no osw.flush(), the file will have some data lost.
                 * Don't know exact reason.
                 */
                osw.flush();
            } catch (IOException e) {
                System.err.println(e);
            }
            builder.setLength(0);
        }
    }

    public void convert(BufferedImage img, String outfile) {
        /* every 39 full-width characters per row; one full-width character is
         * represented by a 8x8 grid of pixel, so scale the orignal image to
         * fit the desired width, 8 * 39 = 312. Height is also scaled to keep
         * the approximate aspect ratio of image; remember that it will be
         * rounded to multiple of 8.
         */
        int newW = 312;
        double scaleW = newW / (double) img.getWidth();
        int newH = (int) ((Math.round(scaleW * img.getHeight()) + 4) & (~0x7));
        double scaleH = newH / (double) img.getHeight();
        mediateImg = new AffineTransformOp(
                AffineTransform.getScaleInstance(scaleW, scaleH),
                AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(img, null);
        quantizeImage();
        try {
            OutputStreamWriter osw = new OutputStreamWriter(
                    new FileOutputStream(outfile), Charset.forName("Big5"));
            transformImage(osw);
        } catch (IOException e) {
            System.err.println(e);
        }
    }

    BufferedImage mediateImage() {
        return mediateImg;
    }
}
